classdef mexHIDUsage < handle
    % @class mexHIDUsage
    % Lookup and reverse lookup of HID usage pages and usages, names and
    % numbers.
    %
    % During development, I used the HID Usages contained in
    % IOHIDUsageTables.h, which is part of the OS X UIKit framework, to
    % build lookuptables.
    %
    % Thereafter, users should not need to care about this, since they can
    % use the tables saved for Matlab in the mexHIDUsageMat file.
    
    properties
        % containers.Map of HID usage page number -> name
        pageNumberToName;
        
        % containers.Map of HID usage page name -> number
        pageNameToNumber;
        
        % Many containers.Map of HID usage number -> name, one for each
        % usage page.  Contained in a containers.Map with page number keys.
        usageNumbersToNames;
        
        % Many containers.Map of HID usage name -> number, one for each
        % usage page.  Contained in a containers.Map with page number keys.
        usageNamesToNumbers;
        
        % Filename with path to .mat file containing mexHIDUsage data.
        mexHIDUsageMat = 'mexHIDUsage.mat';
    end
    
    properties (Access = private)
        % Regular expression to get a page declaration out of the topmost
        % enum.  The tokens are:
        %   - the page name
        %   - the page number in hex
        %   .
        pageEnum = 'kHIDPage_(\w+)\s=\s0x(\w+)';
        
        % Regular expression to get the enum for a particular usage page.
        % The token is:
        %   - the page number in hex
        %   .
        pageUsageEnumFinder = 'Page\s\(0x(\w+)\)\s\*\/';
        
        % Regular expression to get a usage declaration out of an enum.
        % The tokens are:
        %   - Apple's abbreviated usage page name
        %   - the usage name
        %   - the usage number in hex
        %   .
        usageEnum = 'kHIDUsage_([a-zA-Z]+)_(\w+)\s=\s0x(\w+)';
        
        % Regular expression to get a usage declaration out of an enum when
        % there is no Apple abbreviation.  The tokens are:
        %   - the usage name
        %   - the usage number in hex
        %   .
        usageEnumNoAbr = 'kHIDUsage_(\w+)\s=\s0x(\w+)';
    end
    
    methods (Access = private)
        function self = mexHIDUsage
            self.initialize;
        end
    end
    
    methods (Static)
        function u = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                self = mexHIDUsage;
            end
            u = self;
        end
        
        function reset
            self = mexHIDUsage.theObject;
            self.initialize;
        end
        
        function fig = gui
            self = mexHIDUsage.theObject;

            % dump everything into a uitable
            data = cell(0,4);
            pageNumbers = self.allPageNumbers;
            row = 1;
            for ii = 1:length(pageNumbers)
                % a header for the usage page
                pageNum = pageNumbers{ii};
                pageName = mexHIDUsage.nameForPageNumber(pageNum);
                data(row, 1:2) = {pageNum, pageName};
                
                % a bunch of usages on the page
                usages = self.usageNumbersToNames(pageNum);
                nUsages = usages.length;
                data(row+(1:nUsages),3) = usages.keys;
                data(row+(1:nUsages),4) = usages.values;
                
                row = row + nUsages + 1;
            end
            
            fig = figure;
            table = uitable('Parent', fig, ...
                'Units', 'normalized', ...
                'Position', [.05 .05 .9 .9], ...
                'ColumnEditable', false, ...
                'ColumnWidth', {50 150 50 200}, ...
                'ColumnName', {'', 'page', '', 'usage'}, ...
                'RowName', [], ...
                'Data', data);
        end
        
        function pageName = nameForPageNumber(pageNum)
            self = mexHIDUsage.theObject;
            pageName = '';
            if self.pageNumberToName.isKey(pageNum)
                pageName = self.pageNumberToName(pageNum);
            end
        end
        
        function pageNames = allPageNames
            self = mexHIDUsage.theObject;
            pageNames = self.pageNameToNumber.keys;
        end
        
        function pageNum = numberForPageName(pageName)
            self = mexHIDUsage.theObject;
            pageNum = [];
            if self.pageNameToNumber.isKey(pageName)
                pageNum = self.pageNameToNumber(pageName);
            end
        end

        function pageNumbers = allPageNumbers
            self = mexHIDUsage.theObject;
            pageNumbers = self.pageNumberToName.keys;
        end
        
        function usageName = nameForUsageNumberOnPage(usageNum, pageNum)
            self = mexHIDUsage.theObject;
            usageName = '';
            if self.usageNumbersToNames.isKey(pageNum)
                numToName = self.usageNumbersToNames(pageNum);
                if numToName.isKey(usageNum)
                    usageName = numToName(usageNum);
                end
            end
        end
        
        function usageNames = allUsageNamesOnPage(pageNum)
            self = mexHIDUsage.theObject;
            usageNames = {};
            if self.usageNamesToNumbers.isKey(pageNum)
                nameToNum = self.usageNamesToNumbers(pageNum);
                usageNames = nameToNum.keys;
            end
        end
        
        function usageNum = numberForUsageNameOnPage(usageName, pageNum)
            self = mexHIDUsage.theObject;
            usageNum = [];
            if self.usageNamesToNumbers.isKey(pageNum)
                nameToNum = self.usageNamesToNumbers(pageNum);
                if nameToNum.isKey(usageName)
                    usageNum = nameToNum(usageName);
                end
            end
        end

        function usageNums = allUsageNumbersOnPage(pageNum)
            self = mexHIDUsage.theObject;
            usageNums = {};
            if self.usageNumbersToNames.isKey(pageNum)
                numToName = self.usageNumbersToNames(pageNum);
                usageNums = numToName.keys;
            end
        end
    end
    
    methods
        function initialize(self)
            tables = load(self.mexHIDUsageMat);
            self.pageNumberToName = tables.pageNumberToName;
            self.pageNameToNumber = tables.pageNameToNumber;
            self.usageNumbersToNames = tables.usageNumbersToNames;
            self.usageNamesToNumbers = tables.usageNamesToNumbers;
        end
        
        function buildUsageTables(self, IOHIDUsageHeader)
            tables.pageNumberToName = containers.Map(-1, 'a', 'uniformValues', true);
            tables.pageNumberToName.remove(-1);
            
            tables.pageNameToNumber = containers.Map('a', -1, 'uniformValues', true);
            tables.pageNameToNumber.remove('a');
            
            tables.usageNumbersToNames = containers.Map(-1, 'a', 'uniformValues', false);
            tables.usageNumbersToNames.remove(-1);
            
            tables.usageNamesToNumbers = containers.Map(-1, 'a', 'uniformValues', false);
            tables.usageNamesToNumbers.remove(-1);
            
            fid = fopen(IOHIDUsageHeader, 'r');
            try
                currentPage = -1;
                while true
                    line = fgetl(fid);
                    if ~ischar(line)
                        break
                    end
                    
                    % find a usage page declaration
                    tokens = regexp(line, self.pageEnum, 'tokens');
                    if ~isempty(tokens)
                        pageName = tokens{1}{1};
                        pageNum = hex2dec(tokens{1}{2});
                        disp(sprintf('Page %d: %s', pageNum, pageName))
                        
                        % cross reference the page name and number
                        tables.pageNumberToName(pageNum) = pageName;
                        tables.pageNameToNumber(pageName) = pageNum;
                        
                        % new Maps for usages on this page
                        numToName = containers.Map(-1, 'a', 'uniformValues', true);
                        numToName.remove(-1);
                        tables.usageNumbersToNames(pageNum) = numToName;
                        
                        nameToNum = containers.Map('a', -1, 'uniformValues', true);
                        nameToNum.remove('a');
                        tables.usageNamesToNumbers(pageNum) = nameToNum;
                        continue
                    end
                    
                    % find an enum of usages for a page
                    tokens = regexp(line, self.pageUsageEnumFinder, 'tokens');
                    if ~isempty(tokens)
                        % set the page to be filled with usages, below
                        currentPage = hex2dec(tokens{1}{1});
                        disp(sprintf('Page %d:', currentPage))
                        continue
                    end
                    
                    % find a usage declaration
                    tokens = regexp(line, self.usageEnum, 'tokens');
                    if ~isempty(tokens)
                        usageAbr = tokens{1}{1};
                        usageName = tokens{1}{2};
                        usageNum = hex2dec(tokens{1}{3});
                        disp(sprintf(' Usage %d: %s (%s)', usageNum, usageName, usageAbr))
                        
                        % cross reference the usage name and number
                        %   for the current page, found above
                        if tables.usageNumbersToNames.isKey(currentPage)
                            numToName = tables.usageNumbersToNames(currentPage);
                            numToName(usageNum) = usageName;
                            
                            nameToNum = tables.usageNamesToNumbers(currentPage);
                            nameToNum(usageName) = usageNum;
                        end
                        continue
                    end
                    
                    % find a usage declaration
                    tokens = regexp(line, self.usageEnumNoAbr, 'tokens');
                    if ~isempty(tokens)
                        tokens{1}{:}
                        usageName = tokens{1}{1};
                        usageNum = hex2dec(tokens{1}{2});
                        disp(sprintf(' Usage %d: %s', usageNum, usageName))
                        
                        % cross reference the usage name and number
                        %   for the current page, found above
                        if tables.usageNumbersToNames.isKey(currentPage)
                            numToName = tables.usageNumbersToNames(currentPage);
                            numToName(usageNum) = usageName;
                            
                            nameToNum = tables.usageNamesToNumbers(currentPage);
                            nameToNum(usageName) = usageNum;
                        end
                        continue
                    end
                    
                    
                end
            catch erxor
                fclose(fid);
                rethrow(erxor)
            end
            
            fclose(fid);
            save(self.mexHIDUsageMat, '-struct', 'tables');
            self.initialize;
        end
    end
end